/*
 *  Blazon: A user datagram protocol re-braodcasting daemon.
 *  blazon.c: Main program
 *
 *  Copyright 2012 Blazon development team
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.

 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "std-include.h"

#include <signal.h>		/* signal handling */
#include <sys/types.h>	/* needed for kill() */
#include <sys/event.h>	/* kqueue() */
#include <sys/time.h>	/* kqueue() */
#include <sys/socket.h>
#include <netinet/in.h>	/* defines IPPROTO_UDP */
#include <fcntl.h>		/* non-blocking sockets */
#include <stdarg.h>	/* va_list functions */
#include <pwd.h>	/* getpwnam() */
/*#include <pthread.h>*/

#include "blazon.h"
#include "tools.h"
#include "base-io.h"
#include "config.h"
#include "log.h"
#include "io.h"
#include "hash.h"
#include "event.h"

#define MAX_THREADS	5

/* Variables */
volatile sig_atomic_t quit = 0;
volatile sig_atomic_t need_rehash = 0;
struct timeval system_time;
struct hashtable *hash_table = NULL;
struct config_file *config, *newconf;

static void e_write_game_stats(void *);
static void e_log_hashtable_stats(void *);
static void e_clean_hashtable(void *);

/* Codeus maximus complexinatus :\ */

static void
display_usage(void)
{
	printf("blazon [-f|-h|-u|-v]\n");
	printf(" -f run in foreground mode\n");
	printf(" -h show this usage info\n");
	printf(" -u user to run as\n");
	printf(" -v show version\n");
}


static void
display_startup(pid_t proc_id, bool nofork)
{
	printf("blazon: version %s\n", BLAZON_VERSION);
	printf("blazon: pid %d\n", proc_id);
	printf("blazon: running in %s\n",
			nofork ? "foreground" : "background");
}


static void
signal_handler(int signal)
{
	switch (signal) {
		case SIGTERM:
		case SIGINT:
			quit = 1;
			break;
		case SIGHUP:
			need_rehash = 1;
			break;
	}
}


static void
init_scheduled_events(void)
{
	if (config->stats_freq > 0)
		event_add("game_stats", e_write_game_stats, NULL, config->stats_freq*60);
	if (config->hashlog_freq > 0)
		event_addish("log_hashstats", e_log_hashtable_stats, NULL, config->hashlog_freq*60);
	if (config->hashclean_freq > 0)
		event_add("clean_hash_data", e_clean_hashtable, NULL, config->hashclean_freq*60*60);
}


static void
do_rehash(void)
{
	log_main("Received SIGHUP signal: Rehashing configuration");
	if (load_config(&newconf))
	{
		if (!logging_reopen(newconf->logfile, newconf->loglevel))
		{
			log_err("Rehash aborted: Could not open logfile");
			destroy_config(newconf);
		}
		else
		{
			/* If config->listen has changed, all game sockets must
			 * be closed and re-opened.
			 */
			if (strcmp(newconf->listen, config->listen) != 0)
				close_game_sockets(config->game_list);
			else
				update_game_sockets(config, newconf);
			setup_game_sockets(newconf);
			destroy_config(config);
			config = newconf;
			/* Call functions requiring the updated config after this point. */
			clear_all_events();
			init_scheduled_events();
				
			log_main("Rehash successful");
		}
	}
	else
	{
		log_warn("Rehash aborted: Configuration invalid");
	}
	need_rehash = 0;
	newconf = NULL;
}


static void
check_pidfile(void)
{
	FILE *fb;
	char buf[32];
	char *ptr;
	pid_t filepid;

	if((fb = fopen(config->pidfile, "r")) != NULL)
	{
		if((ptr = fgets(buf, 20, fb)) != NULL)
		{
			filepid = atoi(buf);
			if(!kill(filepid, 0))
			{
				printf("blazon: daemon already running\n");
				exit(EXIT_FAILURE);
			}
		}

		fclose(fb);
	}
}


/* Needs to use log functions because it's called after forking */
static void
write_pidfile(void)
{
	FILE *fb;
	char buf[32];
	
	if((fb = fopen(config->pidfile, "w")) != NULL)
	{
		snprintf(buf, sizeof(buf), "%u\n", (unsigned int) getpid());

		if(fputs(buf, fb) == -1)
			log_warn("Error writing to pid file %s (%s)",
					config->pidfile, strerror(errno));
		fflush(fb);
		fclose(fb);
	}
	else
		log_warn("Error opening pid file %s", config->pidfile);
}


static void
do_shutdown(void)
{
	if (config)
	{
		destroy_config(config);
		log_info("Configuration destroyed");
	}

	fdext_close_all();

	/*close_listen_socket();
	close_game_sockets(config);
	log_info("Closed game sockets");*/
	close_sock_raw();
	hashtable_destroy(hash_table, 1);
}


void
die(const char *format, ...)
{
	static char buf[256];
	va_list args;

	if(format == NULL)
		log_err("Unknown termination condition");
	else
	{
		va_start(args, format);
		vsnprintf(buf, sizeof(buf), format, args);
		va_end(args);

		log_err(buf);
	}

	do_shutdown();

	log_err("Blazon terminated: Critical error");
	logging_close();

	exit(EXIT_FAILURE);
}


void
set_time(void)
{
	struct timeval newtime;

	newtime.tv_sec = newtime.tv_usec = 0;

	if(gettimeofday(&newtime, NULL) == -1)
		die("Clock failure.");

	system_time.tv_sec = newtime.tv_sec;
	system_time.tv_usec = newtime.tv_usec;
}


static void
e_write_game_stats(void *unused)
{
	FILE *fp;
	char buf[256];
    struct game_info *game_ptr;
    dlink_node *ptr;

	log_info("Writing stats file.");

	log_info("Number of file descriptors: %d", fd_list_size());

	if ((fp = fopen(config->statsfile, "w")) != NULL)
	{
		snprintf(buf, sizeof(buf), "# Game syntax: <name>, <port>, " \
				"<sent packets>, <throttled packets>\n");

		if (fputs(buf, fp) == -1)
			log_err("Cannot write to stats file %s (%s)",
					config->statsfile, strerror(errno));

		DLINK_FOREACH(ptr, config->game_list.head)
		{
			game_ptr = ptr->data;

			snprintf(buf, sizeof(buf), "%s, %u, %d, %d\n",
					game_ptr->name, game_ptr->port,
					game_ptr->sentcount, game_ptr->throtcount);

			if(fputs(buf, fp) == -1)
			{
				log_err("Cannot write to stats file %s (%s)",
						config->statsfile, strerror(errno));
				break;
			}
		}
		fflush(fp);
		fclose(fp);
	}
	else
		log_warn("Error opening stats file %s", config->statsfile);
}


static void
e_log_hashtable_stats(void *unused)
{
	log_info("-- Hash Table Status --");
	log_info("Number of entries: %u", hash_table->count);
	log_info("Hash table size: %u", (1<<hash_table->bitsize));
	log_info("Table loadlimit: %u", hash_table->loadlimit);
}


/* This function can ONLY free data from a hash table if the <data> struct
 * contains primative data types. */
static void
e_clean_hashtable(void *unused)
{
	unsigned int i, table_len;
	hash_node *next, *ptr;
	struct hashpkt *pkt_p;
	time_t offset;
	void *data;

	if (!hash_table || hash_table->count == 0)
		return;

	log_info("Starting hash table clean-up");

	table_len = (1 << hash_table->bitsize);
	offset = CURRENT_TIME - (config->hashnode_life * 3600);

	for(i = 0; i < table_len; i++)
	{
		int count = 0;

		HNODE_FOREACH_SAFE(ptr, next, hash_table->table[i])
		{
			pkt_p = ptr->data;

			if (pkt_p->timestamp < offset)
			{
				log_debug("Cleaning node #%d '%s' from bucket %d",
						count, ptr->key, i);

				data = hashnode_delete(ptr, &(hash_table->table[i]));
				my_free(data);
				ptr = data = NULL;

				hash_table->count--;
			}
			count++;
		}
	}

	log_info("Done clean-up (%d nodes remaining)", hash_table->count);
}


static void
update_kevent(int kq, fdext_t *F, short filter, int action)
{
	int n;
	struct kevent kev;

	/* fill out the kev struct info ready for adding */
	EV_SET(&kev, F->fd, filter, action, 0, 0, F);

	/* add the struct to kqueue */
	n = kevent(kq, &kev, 1, NULL, 0, NULL);
	if (n < 0 && errno != EBADF)
		log_err("update_kevent() error: %s", strerror(errno));
}


/* EV_ONESHOT deletes events from the kqueue after they've been retrieved */
void
add_kevent(int kq, fdext_t *F, uint8_t type)
{
	if (F == NULL || F->is_open == false)
		return;

	if (type & IO_READ)
	{
		update_kevent(kq, F, EVFILT_READ, EV_ADD|EV_ONESHOT);
	}
	if (type & IO_WRITE)
	{
		update_kevent(kq, F, EVFILT_WRITE, EV_ADD|EV_ONESHOT);
	}
}


/* kqueue() events which are attached to file descriptors are automatically
 * deleted on the last close of the descriptor. */
void
delete_kevent(int kq, fdext_t *F)
{
	if (F == NULL)
		return;

	update_kevent(kq, F, EVFILT_READ|EVFILT_WRITE, EV_DELETE);
}


static void
add_games_to_kqueue(int kq)
{
    struct game_info *game_ptr;
    dlink_node *ptr;

	DLINK_FOREACH(ptr, config->game_list.head)
	{
		game_ptr = ptr->data;

		if (game_ptr->fd_ptr != NULL)
		{
			add_kevent(kq, game_ptr->fd_ptr, IO_READ);
		}
		else
			log_err("Invalid fdext_t pointer for game: %s", game_ptr->name);
	}
}


/* FDE_BIND - Should be a UDP game socket we listen on
 * FDE_LISTEN - Could be listening for new peer connections
 * FDE_SOCKET - Could be an established peer connection
 */
static void
readable_fd(fdext_t *F)
{
	if (F->type & FDE_BIND)
	{
		/* Handle incoming UDP game broadcast packet. */
		/* TODO: If the FD is a game: read the packet, get packet info, check validity of
		packet info, then put into re-send buffer array (ready for the consumer thread).*/
		read_from_socket(F, config->subnets,
				config->subnets_size, hash_table);
		return;
	}
	if (F->type & FDE_LISTEN)
	{
		/* Handle new connection from another blazon instance. */
		return;
	}
	/* Make sure this is last, as by definition an FDE_LISTEN is also an FDE_SOCKET. */
	if (F->type & FDE_SOCKET)
	{
		/* Handle message from another blazon instance. */
		return;
	}
}


static void
main_control_loop(void)
{
	int i, n;
	time_t next;
	struct timespec timeout = {0,0};

	int kq, kqmax = 0;
	struct kevent *kevlist;

	if ((kq = kqueue()) == -1)
		die("kqueue() returned -1");

	kqmax = dlink_list_length(&config->game_list);
	kevlist = my_malloc(sizeof(struct kevent) * kqmax);

	while (quit == 0)
	{
		if (need_rehash)
			do_rehash();

		/* Run any pending events and calculate timeout value. */
		event_run();

		if ((next = event_next()) > 0)
		{
			next -= CURRENT_TIME;
			if (next <= 0)
				timeout.tv_sec = 1;
			else
				timeout.tv_sec = next;
		}
		else
			timeout.tv_sec = 300;

		add_games_to_kqueue(kq);

		n = kevent(kq, NULL, 0, kevlist, kqmax, &timeout);

		set_time();     /* Update the time before we do anything. */

		if (n < 0 && errno != EBADF && errno != EINTR)
			log_err("kevent() error: %s", strerror(errno));
		else if (n > 0)
		{
			for(i = 0; i < n; i++)
			{
				if (kevlist[i].flags & EV_EOF)
				{
					log_warn("kevent() fd %d, filter: %d has EV_EOF set.",
							kevlist[i].ident, kevlist[i].filter);
				}
				if (kevlist[i].flags & EV_ERROR)
				{
					log_err("kevent() error for fd: %d, filter: %d, err: %s",
							kevlist[i].ident, kevlist[i].filter, strerror(errno));
					/* prolly should remove event from the list here */
					/* when an FD is closed, kevents are removed automatically */
					continue;
				}

				if (kevlist[i].filter == EVFILT_READ)
				{
					readable_fd(kevlist[i].udata);
				}
				/*if (kevlist[i].filter == EVFILT_WRITE)
					writeable_fd(kevlist[i].udata);*/
				/*read_from_socket(&(config->games[i]), config->subnets,
						config->subnets_size, hash_table);*/
			}

		}

	} /* end main loop */

	close(kq);
	log_main("Received SIGTERM signal: Shutting down...");
}



int
main(int argc, char *argv[])
{
	char c;
	bool nofork = false;
	const char *uidname = BLAZON_USER;
	struct passwd *pw = NULL;
	pid_t childpid;


	while((c = getopt(argc, argv, "hvfu:")) != -1)
	{
		switch (c)
		{
			case 'f':
				nofork = true;
				break;
			case 'h':
				display_usage();
				exit(0);
				break;
			case 'u':
				uidname = optarg;
				break;
			case 'v':
				printf("blazon: version %s\n", BLAZON_VERSION);
				exit(0);
				break;
			default:
				display_usage();
		}
	}

	log_main("Blazon starting");

	if (geteuid()) {
		die("blazon: root privileges required for socket creation");
	}

	setup_sock_raw();
	log_info("Initialised sock_raw");

	errno = 0;
	if ((pw = getpwnam(uidname)) == NULL)
	{
		die("User '%s' has no passwd entry", uidname);
	}

	if (setgid(pw->pw_gid) != 0)
		die("setgid: Unable to drop group privileges");
	if (setuid(pw->pw_uid) != 0)
		die("setuid: Unable to drop user privileges");
	if (setuid(0) != -1)
		die("Managed to regain root privileges");

	endpwent();


	if (!load_config(&config))
	{
		log_err("Start-up aborted: Error loading config");
		exit(EXIT_FAILURE);
	}

	log_info("Configuration ready");

	/* Need config for pidfile */
	check_pidfile();

	/* Set up logging. */
	if (!logging_open(config->logfile, config->loglevel))
	{
		die("Start-up aborted: Could not open logfile");
	}

	if(nofork == false)
	{
		childpid = fork();

		switch (childpid)
		{
			case -1:
				perror("fork()");
				exit(EXIT_FAILURE);
			case 0:
				close(STDIN_FILENO);
				close(STDOUT_FILENO);
				close(STDERR_FILENO);

				/* Successfully fork()ed */

				if (setsid() == -1)
					die("setsid() error");

				break;
			default:
				display_startup(childpid, nofork);
				return 0;
		}
	}
	else
		display_startup(getpid(), nofork);

	/* Put PID in an external var so other things can use it */
	write_pidfile();

	signal(SIGTERM, signal_handler);
	signal(SIGINT, signal_handler);
	signal(SIGHUP, signal_handler);

	signal(SIGTRAP, SIG_IGN); /* Needed on FreeBSD and possibly others */
	signal(SIGPIPE, SIG_IGN);
	signal(SIGUSR2, SIG_IGN); /* in case of typo */

	set_time();		/* Update the time. */

	setup_game_sockets(config);
	log_info("Initialised game sockets");
	hash_table = hashtable_create(10);
	log_info("Initialised hash table");

	/* Add scheduled events to the scheduler */
	init_scheduled_events();

	/* Enter main IO loop. */
	log_main("Blazon running");
	main_control_loop();

	do_shutdown();
	log_main("Blazon shutdown completed");
	logging_close();

	return EXIT_SUCCESS;
}
